﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text;

namespace LINQPadLogParser
{
  internal sealed class LineManager : IEnumerable<W3CLogLine>
  {
    private const int StepGrow = 10;
    private const int SlowLinesCacheLimit = 10000*StepGrow;
    private readonly CacheItem[] _fastLinesCache = new CacheItem[100];

    private readonly DateTime _from;
    private readonly string _path;
    private readonly DateTime _to;
    private readonly Uri _uri;
    private W3CColumn[] _columns;
    private int _fastLinesCachePosition;
    private CacheItem[] _slowLinesCache = new CacheItem[SlowLinesCacheLimit];
    private int _slowLinesCacheStep = 1;
    private CachedStream _stream;
    private CacheItem _temp = new CacheItem(-1, -2, 0, null);

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public LineManager(string path, Uri uri, DateTime from, DateTime to)
    {
      _path = path;
      _uri = uri;
      _from = @from;
      _to = to;
      if (from == DateTime.MinValue && to == DateTime.MaxValue)
        _stream = new CachedStream(path);
      else
      {
        InitSlice();
      }
    }

    public string Path
    {
      [MethodImpl(MethodImplOptions.AggressiveInlining)] get { return _path; }
    }

    public Uri Uri
    {
      [MethodImpl(MethodImplOptions.AggressiveInlining)] get { return _uri; }
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public IEnumerator<W3CLogLine> GetEnumerator()
    {
      int i = 0;
      while (true)
      {
        W3CLogLine? line = GetLine(i);
        if (line == null)
          yield break;
        yield return line.Value;
        i++;
      }
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    IEnumerator IEnumerable.GetEnumerator()
    {
      return GetEnumerator();
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public W3CLogLine? GetLine(int id)
    {
      CacheItem item = GetLineDescription(id);
      if (item == null)
        return null;
      return new W3CLogLine(item.Id);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public string GetValue(int id, W3CColumn column)
    {
      CacheItem line = GetLineDescription(id);
      if (line == null)
        throw new ApplicationException();
      int index = Array.IndexOf(line.Columns, column);
      if (index < 0)
        return null;
      long offset = index > 0 ? line.Spaces[index - 1] + 1 : 0;
      var count = (int) ((index == line.Columns.Length - 1 ? line.Length : line.Spaces[index]) - offset);
      offset += line.Offset;
      var pageNumber = (int) (offset/CachedStream.PageSize);
      var pageOffset = (int) (offset - pageNumber*CachedStream.PageSize);
      string s = string.Empty;
      while (true)
      {
        byte[] page = _stream.GetPage(pageNumber);
        int c = Math.Min(page.Length - pageOffset, count);
        var chars = new char[c];
        for (int i = 0; i < c; i++)
        {
          chars[i] = (char) page[pageOffset + i];
        }
        s += new string(chars);
        count -= c;
        if (count <= 0)
          break;
        pageNumber++;
        pageOffset = 0;
      }
      s = s == "-" ? null : s.Replace('+', ' ');
      return s;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public W3CColumn[] GetColumns(int id)
    {
      CacheItem line = GetLineDescription(id);
      return line.Columns;
    }

    private void InitSlice()
    {
      using (var fs = new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
      {
        var reader = new StreamReader(fs, Encoding.ASCII);
        string line;
        while (true)
        {
          line = reader.ReadLine();
          const string fields = "#Fields: ";
          Debug.Assert(line != null, "line != null");
          if (line.StartsWith(fields))
          {
            _columns = ParseHeaderLine(line.Remove(0, fields.Length));
            break;
          }
        }
        line = reader.ReadLine();
        Debug.Assert(line != null, "line != null");
        DateTime starts = DateTime.Parse(line.Substring(0, 19), CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal);
        if (starts > _to)
        {
          _stream = new CachedStream(_path, 0, 0);
          return;
        }
        if (starts >= _from)
        {
          _stream = new CachedStream(_path);
          return;
        }
        reader.BaseStream.Position = reader.BaseStream.Length - CachedStream.PageSize;
        reader.DiscardBufferedData();
        reader.ReadLine();
        int i = 0;
        DateTime ends = DateTime.MaxValue;
        while (true)
        {
          i++;
          line = reader.ReadLine();
          if (line == null)
            break;
          if (line[0] == '#')
            continue;

          ends = DateTime.Parse(line.Substring(0, 19), CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal);
        }
        if (i < 2)
          throw new ApplicationException("Last line is too big");
        if (ends < _from)
        {
          _stream = new CachedStream(_path, 0, 0);
          return;
        }
        long from = Find(reader, 0, reader.BaseStream.Length, _from, true);
        long to = ends <= _to ? reader.BaseStream.Length : Find(reader, 0, reader.BaseStream.Length, _to, false);
        _stream = new CachedStream(_path, from, to);
      }
    }

    private long Find(StreamReader reader, long from, long to, DateTime value, bool isMin)
    {
      while (true)
      {
        DateTime time;
        string line;
        if (to - from < CachedStream.PageSize)
        {
          reader.BaseStream.Position = from;
          reader.DiscardBufferedData();
          while (from < to)
          {
            line = reader.ReadLine();
            Debug.Assert(line != null, "line != null");
            from += line.Length + 2;
            if (line[0] == '#')
              continue;
            time = DateTime.Parse(line.Substring(0, 19), CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal);
            if (time > value || (isMin && time == value))
            {
              return from;
            }
          }
          throw new ApplicationException();
        }
        long position = from + (to - from)/2;
        reader.BaseStream.Position = position;
        reader.DiscardBufferedData();
        line = reader.ReadLine();
        Debug.Assert(line != null, "line != null");
        int offset = line.Length + 2;
        line = reader.ReadLine();
        Debug.Assert(line != null, "line != null");
        while (line[0] == '#')
        {
          line = reader.ReadLine();
          Debug.Assert(line != null, "line != null");
          from += line.Length + 2;
        }
        time = DateTime.Parse(line.Substring(0, 19), CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal);
        if (time >= value || (isMin && time == value))
          to = position + offset;
        else
          from = position + offset + line.Length + 2;
      }
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private CacheItem GetLineDescription(int id)
    {
      CacheItem item = FindNearestLine(id);
      while (item.Id < id)
      {
        bool success = ReadNextLine(ref item);
        if (!success)
        {
          return null;
        }
      }
      _fastLinesCachePosition = (_fastLinesCachePosition + 1)%_fastLinesCache.Length;
      _fastLinesCache[_fastLinesCachePosition] = item;

      if (id%_slowLinesCacheStep == 0)
      {
        int index = id/_slowLinesCacheStep;
        _slowLinesCache[index] = item;
        if (index == SlowLinesCacheLimit - 1)
        {
          CompressCache();
        }
      }
      return item;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private void CompressCache()
    {
      var cache = new CacheItem[_slowLinesCache.Length];
      int to = _slowLinesCache.Length/StepGrow;
      for (int i = 0; i < to; i++)
      {
        cache[i] = _slowLinesCache[i*StepGrow];
      }
      _slowLinesCache = cache;
      _slowLinesCacheStep *= StepGrow;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private bool ReadNextLine(ref CacheItem line)
    {
      while (true)
      {
        bool isComment;
        bool success = ReadNextLineInternal(ref line, out isComment);
        if (!success)
        {
          return false;
        }

        if (isComment)
        {
          string s = GetString(line);
          const string fields = "#Fields: ";
          if (s.StartsWith(fields))
          {
            W3CColumn[] columns = ParseHeaderLine(s.Substring(fields.Length));
            line = new CacheItem(line.Id - 1, line.Offset, line.Length, columns, line.Spaces);
          }
          else
          {
            line = new CacheItem(line.Id - 1, line.Offset, line.Length, line.Columns, line.Spaces);
          }
          continue;
        }
        return true;
      }
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private W3CColumn[] ParseHeaderLine(string line)
    {
      string[] strings = line.Split(' ');
      var columns = new W3CColumn[strings.Length];
      int i = 0;
      foreach (string s in strings)
      {
        W3CColumn c;
        switch (s)
        {
          case "date":
            c = W3CColumn.Date;
            break;
          case "time":
            c = W3CColumn.Time;
            break;
          case "c-ip":
            c = W3CColumn.ClientIP;
            break;
          case "cs-username":
            c = W3CColumn.UserName;
            break;
          case "s-sitename":
            c = W3CColumn.SiteName;
            break;
          case "s-computername":
            c = W3CColumn.ComputerName;
            break;
          case "s-ip":
            c = W3CColumn.ServerIP;
            break;
          case "s-port":
            c = W3CColumn.ServerPort;
            break;
          case "cs-method":
            c = W3CColumn.Method;
            break;
          case "cs-uri-stem":
            c = W3CColumn.UriStem;
            break;
          case "cs-uri-query":
            c = W3CColumn.UriQuery;
            break;
          case "sc-status":
            c = W3CColumn.Status;
            break;
          case "sc-substatus":
            c = W3CColumn.SubStatus;
            break;
          case "sc-win32-status":
            c = W3CColumn.Win32Status;
            break;
          case "sc-bytes":
            c = W3CColumn.SentBytes;
            break;
          case "cs-bytes":
            c = W3CColumn.ReceivedBytes;
            break;
          case "time-taken":
            c = W3CColumn.TimeTaken;
            break;
          case "cs-version":
            c = W3CColumn.Version;
            break;
          case "cs-host":
            c = W3CColumn.Host;
            break;
          case "cs(User-Agent)":
            c = W3CColumn.UserAgent;
            break;
          case "cs(Cookie)":
            c = W3CColumn.Cookie;
            break;
          case "cs(Referer)":
            c = W3CColumn.Referer;
            break;
          default:
            throw new ApplicationException(string.Format("Unknown column: {0}", s));
        }
        columns[i] = c;
        i++;
      }
      return columns;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private byte[] GetBytes(CacheItem item)
    {
      var result = new byte[item.Length];
      int count = item.Length;
      var pageNumber = (int) (item.Offset/CachedStream.PageSize);
      var pageOffset = (int) (item.Offset - CachedStream.PageSize*pageNumber);
      while (count > 0)
      {
        byte[] page = _stream.GetPage(pageNumber);
        int length = Math.Min(count, page.Length - pageOffset);
        Array.Copy(page, pageOffset, result, item.Length - count, length);
        count -= length;
        pageOffset = 0;
        pageNumber++;
      }
      return result;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private bool ReadNextLineInternal(ref CacheItem line, out bool isComment)
    {
      int length = -1;
      long offset = line.Offset + line.Length + 2;
      var pageNumber = (int) (offset/CachedStream.PageSize);
      var pageOffset = (int) (offset - CachedStream.PageSize*pageNumber);
      int count = 0;

      byte[] currentPage = _stream.GetPage(pageNumber);
      if (currentPage == null || pageOffset >= currentPage.Length)
      {
        isComment = false;
        return false;
      }
      int[] spaces = null;
      int currentIndex = 0;
      isComment = currentPage[pageOffset] == '#';
      W3CColumn[] columns = null;
      if (!isComment)
      {
        columns = line.Columns;
        if (columns != null)
        {
          spaces = new int[columns.Length - 1];
        }
      }
      while (true)
      {
        for (int i = pageOffset; i < currentPage.Length; i++)
        {
          if (currentPage[i] == 13)
          {
            length = count;
            pageOffset = i + 2;
            break;
          }
          if (currentPage[i] == ' ' && spaces != null && spaces.Length > currentIndex)
          {
            spaces[currentIndex++] = count;
          }
          count++;
        }
        if (length > -1)
          break;
        currentPage = _stream.GetPage(++pageNumber);
        pageOffset = 0;
        if (currentPage == null)
        {
          length = count;
          break;
        }
      }
      if (length <= 0)
      {
        return false;
      }
      while (currentPage != null && pageOffset >= currentPage.Length)
      {
        pageOffset = pageOffset - currentPage.Length;
        currentPage = _stream.GetPage(++pageNumber);
      }
      line = new CacheItem(line.Id + 1, offset, length, columns, spaces);
      return true;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private CacheItem FindNearestLine(int id)
    {
      CacheItem prevLine = _fastLinesCache[_fastLinesCachePosition];
      if (prevLine != null && (prevLine.Id == id || (id != 0 && prevLine.Id == id - 1)))
      {
        return prevLine;
      }
      int min = int.MaxValue;
      _temp = new CacheItem(-1, -2, 0, _columns);
      foreach (CacheItem line in _fastLinesCache)
      {
        if (line == null)
          continue;
        int diff = id - line.Id;
        if (diff == 0)
          return line;
        if (diff >= 0 && diff < min)
        {
          min = diff;
          _temp = line;
        }
      }
      int index = id/_slowLinesCacheStep;
      if (index < _slowLinesCache.Length)
      {
        for (int i = index; i >= 0; i--)
        {
          CacheItem t = _slowLinesCache[i];
          if (t != null)
          {
            return t;
          }
        }
      }
      return _temp;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private string GetString(CacheItem line)
    {
      string s = Encoding.ASCII.GetString(GetBytes(line));
      return s;
    }

    private class CacheItem
    {
      public readonly W3CColumn[] Columns;
      public readonly int Id;

      public readonly int Length;
      public readonly long Offset;
      public readonly int[] Spaces;

      [MethodImpl(MethodImplOptions.AggressiveInlining)]
      public CacheItem(int id, long offset, int length, W3CColumn[] columns, int[] spaces = null)
      {
        Id = id;
        Offset = offset;
        Length = length;
        Columns = columns;
        Spaces = spaces;
      }
    }
  }
}